{
  config,
  lib,
  pkgs,
  ...
}:

let
  # Background information: FastNetMon requires a MongoDB to start. This is because
  # it uses MongoDB to store its configuration. That is, in a normal setup there is
  # one collection with one document.
  # To provide declarative configuration in our NixOS module, this database is
  # completely emptied and replaced on each boot by the fastnetmon-setup service
  # using the configuration backup functionality.

  cfg = config.services.fastnetmon-advanced;
  settingsFormat = pkgs.formats.yaml { };

  # obtain the default configs by starting up ferretdb and fcli in a derivation
  default_configs =
    pkgs.runCommand "default-configs"
      {
        nativeBuildInputs = [
          pkgs.ferretdb
          pkgs.fastnetmon-advanced # for fcli
          pkgs.proot
        ];
      }
      ''
        mkdir ferretdb fastnetmon $out
        FERRETDB_TELEMETRY="disable" FERRETDB_HANDLER="sqlite" FERRETDB_STATE_DIR="$PWD/ferretdb" FERRETDB_SQLITE_URL="file:$PWD/ferretdb/" ferretdb &

        cat << EOF > fastnetmon/fastnetmon.conf
        ${builtins.toJSON {
          mongodb_username = "";
        }}
        EOF
        proot -b fastnetmon:/etc/fastnetmon -0 fcli create_configuration
        proot -b fastnetmon:/etc/fastnetmon -0 fcli set bgp default
        proot -b fastnetmon:/etc/fastnetmon -0 fcli export_configuration backup.tar
        tar -C $out --no-same-owner -xvf backup.tar
      '';

  # merge the user configs into the default configs
  config_tar =
    pkgs.runCommand "fastnetmon-config.tar"
      {
        nativeBuildInputs = with pkgs; [ jq ];
      }
      ''
        jq -s add ${default_configs}/main.json ${pkgs.writeText "main-add.json" (builtins.toJSON cfg.settings)} > main.json
        mkdir hostgroup
        ${lib.concatImapStringsSep "\n" (pos: hostgroup: ''
          jq -s add ${default_configs}/hostgroup/0.json ${pkgs.writeText "hostgroup-${toString (pos - 1)}-add.json" (builtins.toJSON hostgroup)} > hostgroup/${toString (pos - 1)}.json
        '') hostgroups}
        mkdir bgp
        ${lib.concatImapStringsSep "\n" (pos: bgp: ''
          jq -s add ${default_configs}/bgp/0.json ${pkgs.writeText "bgp-${toString (pos - 1)}-add.json" (builtins.toJSON bgp)} > bgp/${toString (pos - 1)}.json
        '') bgpPeers}
        tar -cf $out main.json ${
          lib.concatImapStringsSep " " (pos: _: "hostgroup/${toString (pos - 1)}.json") hostgroups
        } ${lib.concatImapStringsSep " " (pos: _: "bgp/${toString (pos - 1)}.json") bgpPeers}
      '';

  hostgroups = lib.mapAttrsToList (name: hostgroup: { inherit name; } // hostgroup) cfg.hostgroups;
  bgpPeers = lib.mapAttrsToList (name: bgpPeer: { inherit name; } // bgpPeer) cfg.bgpPeers;

in
{
  options.services.fastnetmon-advanced = with lib; {
    enable = mkEnableOption "the fastnetmon-advanced DDoS Protection daemon";

    settings = mkOption {
      description = ''
        Extra configuration options to declaratively load into FastNetMon Advanced.

        See the [FastNetMon Advanced Configuration options reference](https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-configuration-options/) for more details.
      '';
      type = settingsFormat.type;
      default = { };
      example = literalExpression ''
        {
          networks_list = [ "192.0.2.0/24" ];
          gobgp = true;
          gobgp_flow_spec_announces = true;
        }
      '';
    };
    hostgroups = mkOption {
      description = "Hostgroups to declaratively load into FastNetMon Advanced";
      type = types.attrsOf settingsFormat.type;
      default = { };
    };
    bgpPeers = mkOption {
      description = "BGP Peers to declaratively load into FastNetMon Advanced";
      type = types.attrsOf settingsFormat.type;
      default = { };
    };

    enableAdvancedTrafficPersistence = mkOption {
      description = "Store historical flow data in clickhouse";
      type = types.bool;
      default = false;
    };

    traffic_db.settings = mkOption {
      type = settingsFormat.type;
      description = "Additional settings for /etc/fastnetmon/traffic_db.conf";
    };
  };

  config = lib.mkMerge [
    (lib.mkIf cfg.enable {
      environment.systemPackages = with pkgs; [
        fastnetmon-advanced # for fcli
      ];

      environment.etc."fastnetmon/license.lic".source = "/var/lib/fastnetmon/license.lic";
      environment.etc."fastnetmon/gobgpd.conf".source = "/run/fastnetmon/gobgpd.conf";
      environment.etc."fastnetmon/fastnetmon.conf".source = pkgs.writeText "fastnetmon.conf" (
        builtins.toJSON {
          mongodb_username = "";
        }
      );

      services.ferretdb.enable = true;

      systemd.services.fastnetmon-setup = {
        wantedBy = [ "multi-user.target" ];
        after = [ "ferretdb.service" ];
        path = with pkgs; [
          fastnetmon-advanced
          config.systemd.package
        ];
        script = ''
          fcli create_configuration
          fcli delete hostgroup global
          fcli import_configuration ${config_tar}
          systemctl --no-block try-restart fastnetmon
        '';
        serviceConfig.Type = "oneshot";
      };

      systemd.services.fastnetmon = {
        wantedBy = [ "multi-user.target" ];
        after = [
          "ferretdb.service"
          "fastnetmon-setup.service"
          "polkit.service"
        ];
        path = with pkgs; [ iproute2 ];
        unitConfig = {
          # Disable logic which shuts service when we do too many restarts
          # We do restarts from sudo fcli commit and it's expected that we may have many restarts
          # Details: https://github.com/systemd/systemd/issues/2416
          StartLimitInterval = 0;
        };
        serviceConfig = {
          ExecStart = "${pkgs.fastnetmon-advanced}/bin/fastnetmon --log_to_console";

          LimitNOFILE = 65535;
          # Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
          Restart = "on-failure";
          RestartSec = "5s";

          DynamicUser = true;
          CacheDirectory = "fastnetmon";
          RuntimeDirectory = "fastnetmon"; # for gobgpd config
          StateDirectory = "fastnetmon"; # for license file
        };
      };

      security.polkit.enable = true;
      security.polkit.extraConfig = ''
        polkit.addRule(function(action, subject) {
          if (action.id == "org.freedesktop.systemd1.manage-units" &&
            subject.isInGroup("fastnetmon")) {
            if (action.lookup("unit") == "gobgp.service") {
              var verb = action.lookup("verb");
              if (verb == "start" || verb == "stop" || verb == "restart") {
                return polkit.Result.YES;
              }
            }
          }
        });
      '';
      # dbus/polkit with DynamicUser is broken with the default implementation
      services.dbus.implementation = "broker";

      # We don't use the existing gobgp NixOS module and package, because the gobgp
      # version might not be compatible with fastnetmon. Also, the service name
      # _must_ be 'gobgp' and not 'gobgpd', so that fastnetmon can reload the config.
      systemd.services.gobgp = {
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        description = "GoBGP Routing Daemon";
        unitConfig = {
          ConditionPathExists = "/run/fastnetmon/gobgpd.conf";
        };
        serviceConfig = {
          Type = "notify";
          ExecStartPre = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf -d";
          SupplementaryGroups = [ "fastnetmon" ];
          ExecStart = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf --sdnotify";
          ExecReload = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -r";
          DynamicUser = true;
          AmbientCapabilities = "cap_net_bind_service";
        };
      };
    })

    (lib.mkIf (cfg.enable && cfg.enableAdvancedTrafficPersistence) {
      ## Advanced Traffic persistence
      ## https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-traffic-persistency/

      services.clickhouse.enable = true;

      services.fastnetmon-advanced.settings.traffic_db = true;

      services.fastnetmon-advanced.traffic_db.settings = {
        clickhouse_batch_size = lib.mkDefault 1000;
        clickhouse_batch_delay = lib.mkDefault 1;
        traffic_db_host = lib.mkDefault "127.0.0.1";
        traffic_db_port = lib.mkDefault 8100;
        clickhouse_host = lib.mkDefault "127.0.0.1";
        clickhouse_port = lib.mkDefault 9000;
        clickhouse_user = lib.mkDefault "default";
        clickhouse_password = lib.mkDefault "";
      };
      environment.etc."fastnetmon/traffic_db.conf".text = builtins.toJSON cfg.traffic_db.settings;

      systemd.services.traffic_db = {
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        serviceConfig = {
          ExecStart = "${pkgs.fastnetmon-advanced}/bin/traffic_db";
          # Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
          Restart = "on-failure";
          RestartSec = "5s";

          DynamicUser = true;
        };
      };

    })
  ];

  meta.maintainers = lib.teams.wdz.members;
}
